Pour compiler ce document en présentation, exécuter :

jupyter nbconvert Cours\ d’algorithmique.ipynb --to slides --post serve

Bertrand Bordage de NoriPyt

  • Programmeur Python, Django, PostgreSQL
  • Membre de l’équipe de développement du CMS Wagtail
  • Créateur de nombreux projets open source plus ou moins célèbres

Introduction

Algorithme = suite logique d’instructions précises permettant de résoudre un problème à partir de données.

Algorithmique = science et art de structurer un problème complexe pour le résoudre de manière efficace et élégante.

C’est donc le point commun de tous les langages de programmation.

Fermez vos PCs, aujourd’hui on apprend à coder sur papier.

À noter :

  • ce n’est qu’une introduction à l’algorithmique
  • le code sera en langage Python sauf mention du contraire
  • on se limite à la programmation impérative (= ce que vous ferez presque toujours)

Qu’est-ce qu’un langage de programmation ?

Comme une langue :

  • contient des mots & des phrases
  • une syntaxe et une grammaire permettant d’exprimer des idées logiques
  • vous pouvez donner des ordres

Pas comme une langue :

  • très peu de mots
  • pas de conjugaison ou d’accords
  • vous définissez la plupart des mots que vous utilisez
  • approximations impossibles, chaque mot et phrase doit être parfait
  • vous ne pouvez que rarement faire des descriptions
  • personne ne vous répondra jamais

Attention

Langage de programmation ≠ langage déclaratif/descriptif (SQL, HTML, CSS, LaTeX)

Programmation Déclaration/Description
Construis ce meuble ! Ce meuble est bleu et un peu moche.

Les langages de programmation contiennent souvent une partie descriptive, mais la plupart du temps il s’agit donc d’ordres.

On parle de programmation impérative lorsqu’il s’agit essentiellement ou uniquement de donner des ordres.

Langages compilés & interprétés

Pour lancer un programme en langage compilé :

  • le programmeur doit le compiler une fois en langage machine à l’aide d’un compilateur
  • le programme peut ensuite être exécuté directement sans autre programme
  • à chaque changement du programme, le programmeur doit le recompiler

En langage interprété :

  • est exécuté directement, mais à l’aide d’un interpréteur qui détermine quel langage machine exécuter au fur et à mesure de l’exécution

Conclusion un peu cliché

  • un langage compilé :
    • nécessite des compétences solides
    • risque d’erreurs accru
    • un maximum de puissance
  • langage interprété :
    • une puissance réduite
    • considérablement moins de temps de travail
    • adapté aux débutants et experts
    • bien moins de code

Toutefois, de nombreux éléments rendent cette conclusion fausse :

  • compilateurs Just In Time
  • certains langages interprétés plus difficiles (LISP, Scheme)
  • le temps de travail gagné permet d’améliorer ses algorithmes, aboutissant à de meilleures performances que du C mal conçu
  • un langage interprété est souvent plus complexe à totalement maîtriser qu’un langage compilé du fait de sa conception plus technique

Compilé = obsolète & interprété = moderne?

Année Langage
1954 FORTRAN et le premier assembleur compilés
1959 COBOL compilé
1972 C compilé
années 1970 disparition des cartes perforées
1991 Python interprété
1995 Java, Javascript et PHP interprétés

Tous ces langages sont encore indispensables aujourd’hui :

  • Assembleur : base de toute l’informatique
  • FORTRAN : utilisé pour les calculs mathématiques les plus lourds
  • COBOL : les systèmes de transactions bancaires
  • C : la base de tous les programmes de vos ordinateurs & téléphones, et aussi des langages interprétés
  • Python : utilisé partout dans Linux, les sciences « dures », par Google, la NASA, LucasFilm, Instagram, etc
  • Java : utilisé dans d’innombrables systèmes informatiques d’entreprises
  • Javascript : utilisé pour améliorer l’ergonomie des sites, et plus récemment pour écrire des sites entiers
  • PHP : utilisé pour écrire la plupart des sites d’Internet, notamment Facebook

Et oui, les langages les plus anciens encore utilisés aujourd’hui sont en fait les plus indispensables !

Choix du langage

Lourds calculs ou informatique embarquée ⇒ langages compilés

Pour tout le reste ⇒ langages interprétés

Ensuite, un art consistant à choisir le meilleur compromis entre :

  • l’adéquation du langage aux besoins du projet
  • les connaissances de votre équipe et vous-même
  • les possibilités du langage de programmation
  • la qualité du langage (lisibilité, cohérence, philosophie)
  • l’avenir du langage

Mes recommandations après 22 ans d’expérience sur une vingtaine de langages :

  • Apprendre un langage interprété simple et permettant de tout faire : typiquement Python.
  • Connaître les bases d’un langage bas niveau : typiquement C (et non C++).
  • Apprendre un langage dominant dans votre branche : pour du web, Javascript.

Structures de données

3 est une donnée. « J’aime coder » aussi.

Comment les stocker ?

Deux manières :

  • constante pour les valeurs qui ne changeront jamais (nom du programme, version, etc)
  • variable pour tout le reste

Il faut donner un nom, sans espaces ni caractères spéciaux, juste de l’ASCII.

Certaines données peuvent être très simples, comme un nombre.

D’autres peuvent être très complexes, comme un index de base de données sous forme d’arborescence.

Types de données

Les données sont le premier outil du programmeur.

Chaque donnée est typée et possède des règles propres à son type.

Quelques types prédéfinis, à partir desquels tout est possible.

Vous les connaissez sans doute, mais mettons maintenant des noms dessus.

Types élementaires

Les types de base sont presque toujours les mêmes :

Booléens : uniquement True ou False

Nombres entiers : 3, -8752, 654189426871662371897286541987153419827154

Nombres à virgule (flottants) : 1.0, -5e19, 8e-20, NaN, +inf, -inf

Chaînes de caractères : 'a', 'Bonjour', 'βατρακοι'

Des tas d’autres types disponibles dans les langages : fichiers, dates, ensembles, nombres complexes, expressions régulières.

Instructions

Un programme est constitué d’instructions. Exemples d’instructions :

  • faire une addition
  • afficher un texte à l’écran
  • lire un fichier

Séquence

La plupart du temps, les instructions s’exécutent en séquence. Exemple de séquence d’instructions :

  • se lever
  • se préparer
  • petit-déjeuner
  • aller au travail

Opérations

Les opérations et leurs opérateurs varient assez peu d’un langage à l’autre.

L’opération la plus élémentaire est l’assignation, elle permet de stocker une donnée dans une variable ou une constante. Le plus souvent =, mais parfois := ou autre. À différencier de l’égalité mathématique.


In [ ]:
une_variable = 5

Cette opération d’assignation est un peu différente des autres :

Type d’opération Opérateur Exemples Résultats
Combinaison + - * / % 2 + 7 9
Comparaison == != > < >= <= 9 == 9 True
Combinaison booléenne and or not ou && || ! True and False False
Combinaison binaire & | ^ ~ 0b0011 & 0b1100 0b0000

Des raccourcis existent dans la plupart des langages, par exemple :


In [ ]:
x = 3
x += 7
x *= 8

Équivaut parfaitement à :


In [ ]:
x = 3
x = x + 7
x = x * 8

Exercice 1

Définir la constante mathématique π avec une précision de 2 chiffres après la virgule.

Exercice 2

Que vaut x après ces trois lignes ?

x = 27
x -= 2
x / 5

Exercice 3

Tester si « é » est différent de « e ».

Exercice 4

Définir une variable x valant 333, la diviser par 3, retirer 11 et diviser par 10.

Ceci doit être fait en deux instructions.

Combien vaut x à la fin ?

Types composés

Un type composé rassemble des valeurs d’autres types en un seul élément.

À partir des types simples que nous verrons, il est possible de faire des types plus complexes : arborescences, graphs circulaires, modèles de base de données, etc.

Liste / tableau

Le type composé le plus simple est la liste, aussi appelée array (=tableau) selon les langages. Elle est indexée en commençant par 0.

5008003
012
012
0123
1456
2789

Dans les langages bas niveau, la liste n’accepte qu’un type de données.

Exemple C :


In [ ]:
int data[3] = {500, 800, 3};
data[0]  // 500
float data[3][3] = {{1.0, 2.0, 3.0},
                    {4.0, 5.0, 6.0},
                    {7.0, 8.0, 9.0}};
data[1][1]  // 5

Exemple Python :


In [ ]:
data = [500, 800, 3]
data[0]  # 500
data = [[1.0, 2.0, 3.0],
        [4.0, 5.0, 6.0],
        [7.0, 8.0, 9.0]]
data[1][1]  # 5
data = ['a', 1, True]
data[2]  # True

Exercice 5

Si l = [1, [2, 3], [4], 5] Quelle est la valeur de l[2] ? La valeur de l[1][0] ? Et enfin l[3][1] ?

Exercice 6

Si l = [[8, 3, 7], [12, 2, 91], [830, 6, 57]]

Écrire le code nécessaire pour obtenir le nombre 91 à partir de l.

Dictionnaires

Appelés objets en Javascript ou listes d’associations en LISP & Scheme.

Proche de la liste, mais :

  • fait correspondre des valeurs deux par deux
  • n’est pas ordonné (selon les langages)

Exemple Python / Javascript :


In [ ]:
ages = {
    'Jean-Paul': 42,
    'Zoé': 12,
    'Lucas': 27,
}

Structures et classes

Structure

Proche du dictionnaire, mais avec structure fixe. Extrêmement utilisée en C.


In [ ]:
struct Book {
    char[50] title
    char[50] subtitle
    char[150] authors
    int length
}

Classe

Comme une structure, mais avec des fonctions liées (nommées méthodes), beaucoup plus facile à utiliser :


In [1]:
class Book:
    def __init__(self, title, subtitle='', authors='', length=None):
        self.title = title
        self.subtitle = subtitle
        self.authors = authors
        self.length = length

    def __str__(self):
        return '« %s » par %s' % (self.title, self.authors)

print(Book('Les Piliers de la terre', authors='Ken Follet'))


« Les Piliers de la terre » par Ken Follet

Structures de contrôle

La séquence est la structure de contrôle par défaut. Bien entendu, elle n’est pas pratique, on ne peut se contenter de toujours suivre le même circuit.

Il existe principalement deux autres structures de contrôle.

Conditions

Si en retard, pas de petit déjeuner !

  • se lever
  • se préparer
  • si je ne suis pas en retard :
    • petit-déjeuner
  • aller au travail

Boucles

Une fois sur le lieu de travail, il faut travailler jusqu’à l’heure de sortie :

  • tant que l’heure est inférieure à la fin de journée de travail :
    • travailler deux heures
    • faire une pause de 5 minutes
  • rentrer à la maison

Ensuite, chez moi, je dois faire la vaisselle. On peut dire :

  • tant qu’il reste des assiettes à laver dans l’évier :
    • je prends une assiette
    • je lave l’assiette

Mais pour être plus rapide et lisible, il est préférable de dire :

  • pour chaque assiette dans l’évier :
    • je la nettoie

Voyons comment ceci se traduit en algorithmique.

Conditions

Cela permet de faire varier les instructions en fonction des données.

Une condition exécute un bloc d’instructions si la condition est vraie.


In [ ]:
name = input('Bonjour, quel est votre nom ? ')
if name == 'Germaine':
    print('Bonjour arrière-grand-mère !')
elif name == 'Jean-Marie':
    print('Bonjour tonton !')
else:
    print('Bonjour ' + name + ' !')

Exercice 7

Quelle est la valeur de x à la fin de l’exécution du code ?


In [ ]:
x = 7
if x / 3.5 >= 2:
    x += 9
    if x * 3 > 17:
        x /= 8
else:
    x -= 2

Boucles

Pour itérer sur des objets (= les passer en revue), par exemple dans une liste :


In [ ]:
maximum = 0
for number in [7, 28, 4, 9, 12, 3]:
    if number > maximum:
        maximum = number

Ceci fonctionne dans quelques langages de haut niveau comme Python, mais dans la plupart des langages, il faut créer un index intermédiaire. Par exemple en C :


In [ ]:
void main() {
    int maximum = 0;
    int data[6] = {7, 28, 4, 9, 12, 3};
    for (int i = 0; i < 6; i++) {
        int number = data[i];
        if (number > maximum) {
            maximum = number;
        }
    }
}

Dans les autres cas :


In [ ]:
while input('Avez-vous fini ? ') not in ('oui', 'o', 'OUI', 'O', 'Oui'):
    print('Écrivez oui, tout va bien se passer.')

Ou :


In [ ]:
while True:
    if input('Avez-vous fini ? ') in ('oui', 'o', 'OUI', 'O', 'Oui'):
        break
    print('Écrivez oui, tout va bien se passer.')

Exercice 8

Écrire une boucle trouvant le plus grand commun diviseur entre deux nombres.

Exercice 9

Quelle est la valeur de x à la fin de l’exécution ?


In [ ]:
x = 0
i = 5
while i > -2:
    x += i
    i -= 1

Fonctions et procédures

Fonction

Groupe de lignes de code effectuant une tâche donnée, à laquelle nous donnons un nom. Une fonction renvoie un résultat et ne modifie pas l’état du programme ou les données d’entrée.


In [ ]:
def function(n: int) -> int:
    return n * 4 + 2

Procédure

Même chose que fonction, mais modifie l’état du programme et/ou des données d’entrée, et peut ne rien renvoyer.


In [ ]:
def procedure(l: list):
    l[0] = 3
    print('Nous avons modifié la liste en entrée')

Un paradigme nommé programmation fonctionnelle consiste à n’utiliser que des fonctions et jamais de procédure. En pratique, ce paradigme est rarement utilisé seul, car il empêche toute interaction avec le système d’exploitation (et donc avec fichiers, réseaux, périphériques)

Exercice 10

Dans chaque cas, est-ce une fonction ou une procédure ?


In [ ]:
def martin(first_name: str):
    return first_name + ' Martin'

In [ ]:
def martin_bis():
    first_name = input('Quel est votre prénom, monsieur Martin ?')
    return first_name + ' Martin'

In [ ]:
def f(n: int):
    print('Bonjour')
    return n + 5

Forme récursive

Une fonction ou procédure peut s’appeler elle-même.

Utilité :

  • traverser des arborescences
  • effectuer des suites mathématiques

Exemple avec une arborescence :


In [ ]:
def find_min_max(tree: list):
    minimum = float('inf')
    maximum = 0
    for value in tree:
        if isinstance(value, list):
            value_min, value_max = find_min_max(value)
            if value_min < minimum:
                minimum = value_min
            if value_max > maximum:
                maximum = value_max
        else:
            if value < minimum:
                minimum = value
            if value > maximum:
                maximum = value
    return minimum, maximum
            

find_min_max([
    [827, 312, [592, 522]],
    910, 395, [815],
    [195, 168, [806, 497, [972]]],
])

Exercice 11

Quel résultat va donner cette fonction pour i = 1 ? et i = 3 ? et i = 7 ?


In [ ]:
def fib(i: int) -> int:
    if i < 2:
        return i
    return fib(i-1) + fib(i - 2)

Exercice 12

Écrire la fonction factorielle en utilisant de la récursivité. Une fois fini, écrire la même fonction sans récursivité et comparer les résultats

Règles d’or du meilleur dev

Désolé pour vous, il faut faire des efforts.

Vous devrez vous construire ces habitudes avec le temps pour être un dev de qualité supérieure :

  • Réfléchir à sa structure avant de commencer à coder.
  • Au moindre blocage, utiliser la méthode du canard en plastique ou un papier et un crayon .
  • Donner des noms compréhensibles et courts aux variables et fonctions.
  • Ne jamais copier-coller de code, surtout s’il est pompé sur Internet.
  • Utiliser systématiquement un gestionnaire de versions.
  • Ne jamais commenter une ligne et la laisser « parce que ça pourrait toujours servir ». Ça servira juste à dérouter la personne qui le relira, peut-être vous.
  • Il faut mieux passer du temps à clarifier et simplifier son code qu’à le commenter outrageusement.
  • Respecter les conventions en usage, en priorité celles du projet, puis celles du langage.

De manière générale :

  • Ne travaillez pas dur, travaillez intelligemment.
  • Soyez propres, vous vous remercierez vous-même dans quelques années en reprenant un projet.

Structurer son code

L’erreur du débutant : écrire une énorme fonction devenant incompréhensible pour une tâche globale comme mon_jeu().

Structurer son code = subdiviser une vaste tâche en tâches élémentaires, et grouper ces tâches de manière censées.

Outils à disposition :

  • constantes et variables globales
  • fonctions & procédures
  • classes et structures (si le langage les supporte)
  • modules

Un module est un fichier regroupant des fonctions, des constantes, des classes, etc.

Un module peut aussi être un dossier regroupant plusieurs sous-modules et ainsi de suite.

Structurer son code permet donc de créer une arborescence de module qui soit claire pour les développeurs l’utilisant.

Librairie = module partagé par quelqu’un pour que d’autres puissent s’en servir facilement

Une librairie est généralement installée une seule fois sur le système, et plusieurs programmes peuvent s’en servir sans avoir à la réinstaller à chaque fois.

Exemple de module utilisant une librairie

Module mon_jeu
Procédure lancer_jeu()
Procédure afficher_bouton_quand_appuyé()
Librairie jeu_video
Module manettes
Constante NOMBRE_MAXIMUM_DE_MANETTES
Variable nombre_de_manettes
Procédure allumer_manettes()
Procédure éteindre_manettes()
Fonction afficher_nom(manette)
Fonction lister_boutons(manette)
Classe Manette
Attribut nom
Attribut boutons
Méthode allumer()
Méthode éteindre()
Module video
Et cetera

Architecture à trois couches

Aussi appelée « architecture trois tiers »

Omniprésente en informatique aujourd’hui. Utilisée par les applications de bureau ou mobiles et sur les sites Internet.

Les couches sont :

  • affichage des données
  • traitement des données
  • stockage persistent de données

Exemples

Couche Site Internet Tableur de bureau
Affichage Navigateur web Fenêtre avec cases, menus, boutons
Traitement Serveur applicatif Calcul du contenu des cellules
Stockage Base de données SQL Fichier ODS, XLS, CSV

À retenir, ce sera le point de départ de la plupart de vos projets.

Modélisation

Modélisation d’un algorithme = l’écrire visuellement, de la manière qui nous parle le mieux

⇒ Pas de manière particulière de noter, faisons ce qui paraît clair.

Quelques problèmes concrets en organigrammes puis traduits en code :

Manger

Équivalent en code


In [ ]:
def manger(bras, bouche):
    lever(bras)
    ouvrir(bouche)
    fermer(bouche)
    while est_pleine(bouche):
        ouvrir(bouche)
        fermer(bouche)

Ou encore :


In [ ]:
def manger(bras, bouche):
    lever(bras)
    while True:
        ouvrir(bouche)
        fermer(bouche)
        if not est_pleine(bouche):
            break

Évidemment, lever, ouvrir, fermer et est_pleine restent à définir.

Prenons un cas plus abstrait mais plus précis.

Trouver le minimum

Équivalent en code


In [ ]:
def trouver_minimum(l):
    minimum = float('+inf')
    for i in l:
        if i < minimum:
            minimum = i
    return minimum

Organigramme plus clair pour débuter, mais bien plus long et compliqué à lire quand on est habitué au code.

Ne pas hésiter à se faire des organigrammes pour mettre les choses à plat.

Oui, je m’en sers encore de temps en temps.

Complexité algorithmique

Sert à identifier un algorithme lent, puis à comprendre pourquoi et trouver comment l’améliorer.

On utilise la notation O() pour décrire le temps d’exécution d’une fonction ou procédure dans le pire des cas.

Pour une fonction ou procédure, on dit qu’elle est :

  • O(1) si elle prend toujours le même temps, quelque soit son contenu.
  • O(n) si elle prend un temps linéairement proportionnel à la valeur d’un argument
  • O(2n) si elle prend un temps proportionnel à 2 fois la la valeur d’un argument
  • O(n²) si elle prend un temps proportionnel au carré de la valeur d’un argument
  • O(m×n) si elle prend un temps proportionnel au produit des valeurs de deux arguments
  • O(log n) si elle prend un temps logarithmiquement proportionnel à la valeur d’un argument
  • Et cetera

De même que pour noter le pire des cas, la notation Ω() permet de décrire le meilleur des cas.

Il existe d’autres notations pour décrire d’autres complexités : la mémoire utilisée, le nombre d’opérations effectuées, etc.

Exemples


In [ ]:
def f(n: int) -> int:
    return 3 * n

Complexité : O(1)


In [ ]:
def f(n: int) -> int:
    total = 0
    for i in range(n):
        total += i
    return total

Complexité : O(n)


In [ ]:
def f(m: int, n: int) -> int:
    total = 0
    for x in range(m):
        for y in range(n):
            total += x * y
    return total

Complexité : O(m×n)

Exercice 13

Quelle est la complexité de ces fonctions ?


In [ ]:
def f() -> int:
    return 3

In [ ]:
def f(m: int, n: int) -> int:
    total = 0
    for i in range(m):
        total += i * n
    return total

In [ ]:
def f(n: int) -> int:
    total = 1
    for x in range(n):
        for y in range(n):
            total *= n
        total /= 2
    return total

Exercice 14

Écrire une fonction de complexité O(2n).

Efficacité d’algorithmes pour un même problème

Souvent les problèmes les plus simples sont les plus intéressants à étudier, car ils aboutissent à des gains de performance sur l’ensemble de l’informatique.

Exemple typique : tri d’une liste.

Opération utile partout et pourtant il est facile de la faire inefficacement.


In [1]:
DATA = [7, 10, 3, 9, 6, 0, 4, 13, 2, 12, 8, 11, 5, 1]

Tri par sélection

Ω(n²) (meilleur des cas)

O(n²) (pire des cas)


In [2]:
def tri(l: list) -> list:
    n = len(l)
    for i1 in range(n):
        minimum = i1
        for i2 in range(i1+1, n):
            if l[i2] < l[minimum]:
                minimum = i2
        if minimum != i1:
            l[i1], l[minimum] = l[minimum], l[i1]
    return l

In [3]:
print(tri(DATA.copy()))
%timeit tri(DATA.copy())


[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
17.9 µs ± 602 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Tri par insertion

Ω(n) (meilleur des cas)

O(n²) (pire des cas)


In [4]:
def tri(l: list) -> list:
    n = len(l)
    for i1 in range(n):
        v1 = l[i1]
        i2 = i1
        while i2 > 0 and l[i2 - 1] > v1:
            l[i2] = l[i2 - 1]
            i2 -= 1
        l[i2] = v1
    return l

In [5]:
print(tri(DATA.copy()))
%timeit tri(DATA.copy())


[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
13.4 µs ± 366 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Tri par tas

Ω(n×log(n))

O(n×log(n))


In [6]:
def descendre(l: list, premier: int, dernier: int):
    maximum = 2 * premier + 1
    while maximum <= dernier:
        if maximum < dernier and l[maximum] < l[maximum + 1]:
            maximum += 1
        if l[maximum] > l[premier]:
            l[maximum], l[premier] = l[premier], l[maximum]
            premier = maximum
            maximum = 2 * premier + 1
        else:
            break


def tri(l: list) -> list:
    n = len(l) - 1
    for i in range(int(n / 2), -1, -1):
        descendre(l, i, n)

    for i in range(n, 0, -1):
        if l[0] > l[i]:
            l[0], l[i] = l[i], l[0]
            descendre(l, 0, i-1)
    return l

In [7]:
print(tri(DATA.copy()))
%timeit tri(DATA.copy())


[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
28.2 µs ± 2.06 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Plus lent que les algorithmes moins efficaces !?

Première possibilité : nous sommes dans le pire des cas du tri par tas mais le meilleur des cas pour les autres algorithmes

Seconde possibilité : notre implémentation est trop lente.

Regardez le tri par tas, son exécution demande d’exécuter beaucoup plus d’instructions que les autres algorithmes.

Les détails d’implémentation sont très importants, c’est pourquoi l’algorithmique doit être combinée à une excellente connaissance du langage en question.

L’algorithme de tri de Python, de même complexité algorithmique :


In [8]:
l = DATA.copy()
l.sort()
print(l)
l = DATA.copy()
%timeit l.sort()


[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
350 ns ± 41.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Merci &
bon courage pour la suite !

Suivez nos activités sur
GitHub: @BertrandBordage
Twitter: @NoriPytCom